其实DAL最难理解的就是,函数里面嵌套函数,将函数抽象为参数传递到函数中,并且函数参数里面还有参数,这是我最陌生的地方。只要多接触,自然就熟悉了。
如果只是单纯的函数作为参数传递进来,我还是可以理解的:
xxxxxxxxxx11export async function dalRequireAuthOperation(operation: () => void) {2 const session = await auth.api.getSession({3 headers: await headers(),4 });56 if (session) {7 return operation();8 }9}但如果函数作为参数,并且函数参数里面定义有参数和返回值类型,我看上去就很难理解了:
x
1export async function dalRequireAuth<T>(2 operation: (session: SessionType) => Promise<DalReturn<T>>,3) {4 const session = await auth.api.getSession({5 headers: await headers(), // you need to pass the headers object.6 });78 if (!session) {9 return createErrorReturn({10 type: "no-user",11 });12 }1314 return operation(session);15}其实仔细拆解一下,就会发现,operation就是一个函数,并且在调用operation函数的时候,可以接收到传递过来的session参数,就是这里难以理解。这就是闭包的概念。但是这里非常抽象,就是operation其实我不知道是什么样的,
参考webdevsimplied这个老师的做法。这个DAL是每个项目都必须的,必须把这个功能加上。
老师的项目使用了sqlite3,所以不需要专门的数据库,使用npm install,然后运行drizzle相关的命令即可。重点关注两个页面:/todos和/admin。
有_naive、_dal-basic、_dal-advanced三种类型的文件,分别对应的是不同的data access layer写法,可以将其替换到page.tsx里面看效果。

action函数也有data access layer,也是分为三种类型,可以对比看。

naive里面的代码就不要参考了,这是不推荐的写法,但是dal-advanced里面的封装太复杂了,这个暂时还看不懂,所以直接参考dal-basic的写法,能够写成这样就已经很不错了。dal-advanced里面的代码要慢慢学会写。
naive的写法就是在页面中直接进行用户权限检查和数据库查询,这样会导致大量的样板代码,如果直接在页面查询,很容易忘记添加权限检查,从而导致安全漏洞(例如普通用户访问了管理员数据)。反之将数据获取逻辑封装在函数中,可以更方便地统一应用 Next.js 的缓存机制(如 cache 或 revalidateTag)。
下面这样的代码就不要写了:

最起码要写成这样的,要先封装一个函数,然后在页面中调用(也就相当于把检查用户是否登录的语句放到封装函数里面了):


仔细了解一下之后,好像dal-basic也太简单了一点,dal-basic的mutations和queries里面对user的校验代码也很重复,而且还是手动抛出错误,但就是简单,别人一看代码就明白了。
dal-advanced的代码封装,太抽象了一点,我看了很久,很难总结出它到底做了什么,我问一下AI:
在视频中,Kyle 介绍的
dal-advanced(高级数据访问层)模式,核心思想是将数据获取逻辑从“直接抛出错误/重定向”转变为“返回结构化的结果对象”。这种模式模仿了类似 Rust 的
Result类型或验证库(如 Zod)的返回结构。以下是该模式代码设计的详细拆解:
- 核心数据结构:Result 对象
在
dal-advanced中,DAL 函数不再直接返回数据,而是返回一个包含状态的对象。其 TypeScript 定义大致如下:xxxxxxxxxx261// 这里的extends的意思是,传过来的E的类型,必须长得有DalError这样子。2export type DalReturn<T, E extends DalError = DalError> =3| {4success: true5data: T6}7| {8success: false9error: E10}1112export type DalError =13| {14type: "no-user"15}16| {17type: "no-access"18}19| {20type: "drizzle-error"21error: DrizzleQueryError22}23| {24type: "unknown-error"25error: unknown26}为什么要这么做?
- 可预测性:调用者通过检查
success属性,就能明确知道操作是否成功,而不需要用try/catch去捕获可能发生的重定向或错误。- 逻辑解耦:DAL 层只负责检查权限和拿数据,不决定出错后该怎么办(是跳转页面、弹窗还是返回 401 状态码)。
- 标准化的 DAL 函数写法
一个高级 DAL 函数(如获取用户待办事项)的代码结构通常如下:
xxxxxxxxxx211export async function getTodos() {2const user = await getCurrentUser(); // 获取当前用户34// 1. 权限检查:不直接 redirect,而是返回错误类型5if (!user) {6return { success: false, error: 'NOT_AUTHENTICATED' } as const;7}89try {10// 2. 数据库查询11const data = await db.query.todos.findMany({12where: eq(todos.userId, user.id)13});1415// 3. 成功返回16return { success: true, data };17} catch (e) {18// 4. 异常处理19return { success: false, error: 'UNKNOWN_ERROR' } as const;20}21}
- 调用层的灵活处理(分层消费)
这是
dal-advanced最强大的地方,不同的调用方可以根据返回的error做出不同反应:场景 A:在 React Server Component (页面) 中
如果你希望用户没登录就直接跳到登录页,可以使用一个辅助函数
verifyData:xxxxxxxxxx91// 页面组件2export default async function TodosPage() {3const result = await getTodos();45// 辅助函数:如果 success 为 false,内部执行 redirect('/login')6const data = verifyData(result);78return <div>{/* 渲染 data */}</div>;9}场景 B:在 API Route 或 Server Action 中
如果你正在处理一个异步请求,不希望页面跳转,而是想给前端一个 JSON 错误:
xxxxxxxxxx121export async function POST() {2const result = await getTodos();34if (!result.success) {5if (result.error === 'NOT_AUTHENTICATED') {6return new Response("请先登录", { status: 401 });7}8return new Response("出错了", { status: 500 });9}1011return Response.json(result.data);12}
- 进阶:使用 Wrapper 减少冗余
视频中还提到了创建一个通用的包装器(如
dbWrapper),自动处理数据库错误捕获:xxxxxxxxxx91async function dbOperation<T>(operation: () => Promise<T>): Promise<DALResult<T>> {2try {3const data = await operation();4return { success: true, data };5} catch (error) {6console.error(error);7return { success: false, error: 'UNKNOWN_ERROR' };8}9}总结
dal-advanced的代码优势:
- 强类型支持:通过 TypeScript,你在处理
result.data之前必须先判断result.success,减少了空指针崩溃。- 错误分类:将“未登录”、“无权限”和“数据库崩溃”明确区分开。
- 多环境复用:同一套 DAL 代码可以无缝用于页面渲染、后台接口和自动化脚本,因为它的逻辑是“纯粹”的返回,不依赖于 Next.js 的路由环境。
这种模式虽然增加了一些样板代码(每个函数都要返回对象),但在中大型项目中,它带来的可测试性和安全性远超基础模式。
这不就是我一直寻找的问题答案吗?我之前一直担心nextjs里面的数据操作,怎么处理错误?要不要写try...catch?写了try...catch之后,boilerplate代码太多了,显示不出我的能力啊。这个dal-advanced不就解决了问题嘛。
多看几遍,一遍肯定是看不懂的。看懂了之后,自己写几遍。
这个文件里面是一套封装函数,这段代码本质上是一套工具集,目的是让你的 Next.js 应用在处理数据库请求时,既安全又灵活。
核心原理是:将数据操作变成一个可以被层层拦截、层层处理的“管道”。
我们可以把这些函数拆分为 执行者 - 处理数据库(dalDbOperation)、动作适配器 - 处理结果(Redirect/Throw/VerifySuccess)、守门员/保镖(RequireAuth)和辅助工具(Format)。
dalDbOperation<T>(operation)
xxxxxxxxxx161export async function dalDbOperation<T>(operation: () => Promise<T>) {2 try {3 // 先执行operation函数,如果正常,那么就返回data4 const data = await operation();5 return createSuccessReturn(data);6 } catch (e) {7 // 这里的e就是错误,返回不同的错误即可8 if (e instanceof ThrowableDalError) {9 return createErrorReturn(e.dalError);10 }11 if (e instanceof DrizzleQueryError) {12 return createErrorReturn({ type: "drizzle-error", error: e });13 }14 return createErrorReturn({ type: "unknown-error", error: e });15 }16}这是所有数据库操作的“防护壳”。
try/catch 紧紧包裹住数据库操作。如果是数据库报错,它会把它包装成一个友好的错误对象返回( { success: false, error: ... }),而不是让整个页面崩掉(White Screen of Death)。dalRequireAuth<T>(operation, options)
xxxxxxxxxx161export async function dalRequireAuth<T, E extends DalError>(2 operation: (user: typeof UserTable.$inferSelect) => Promise<DalReturn<T, E>>,3 { allowedRoles }: { allowedRoles?: UserRole[] } = {},4) {5 const user = await getCurrentUser();67 if (user == null) {8 return createErrorReturn({ type: "no-user" });9 }1011 if (allowedRoles && !allowedRoles.includes(user.role)) {12 return createErrorReturn({ type: "no-access" });13 }1415 return operation(user);16}这是最核心的函数,用于定义一个受保护的数据操作。
写法:它接收你真正想做的操作(operation函数参数),以及允许访问的角色列表allowedRoles(是一个对象里面的数组,为什么不直接传递数组,可能原因是这里可以直接传递session数据,这个数据里面有allowedRoles数组,更加方便了)。
作用:
getCurrentUser)。no-user 错误,不执行数据库查询。no-access 错误。价值:它把“鉴权”和“取数”解耦了,你的数据库查询代码里不需要再写一遍权限检查。你再也不用在每个数据库函数里重复写 if (user == null) ... 了。
这组函数决定了当 DAL 返回结果后,程序该表现出什么行为。
dalLoginRedirect(dalReturn)
x
1export function dalLoginRedirect<T, E extends DalError>(2 dalReturn: DalReturn<T, E>,3) {4 if (dalReturn.success) return dalReturn;5 if (dalReturn.error.type === "no-user") return redirect("/login");67 return dalReturn as DalReturn<T, Exclude<E, { type: "no-user" }>>;8}no-user 错误,直接调用 Next.js 的 redirect("/login")。Exclude方法。如果函数执行完没跳转,TS 会自动推断出剩下的结果里一定不会再有“未登录”这个错误类型。dalUnauthorizedRedirect(dalReturn, path)
xxxxxxxxxx91export function dalUnauthorizedRedirect<T, E extends DalError>(2 dalReturn: DalReturn<T, E>,3 redirectPath = "/",4) {5 if (dalReturn.success) return dalReturn;6 if (dalReturn.error.type === "no-access") return redirect(redirectPath);78 return dalReturn as DalReturn<T, Exclude<E, { type: "no-access" }>>;9}no-access(没权限),直接跳转到首页(或你指定的路径)。dalThrowError(dalReturn)
xxxxxxxxxx71export function dalThrowError<T, E extends DalError>(2 dalReturn: DalReturn<T, E>,3) {4 if (dalReturn.success) return dalReturn;56 throw dalReturn.error;7}error.js 错误页面。dalVerifySuccess(dalReturn, options) —— 终极聚合函数
x
1export function dalVerifySuccess<T, E extends DalError>(2 dalReturn: DalReturn<T, E>,3 { unauthorizedRedirectPath }: { unauthorizedRedirectPath?: string } = {},4): T {5 const res = dalThrowError(6 dalUnauthorizedRedirect(7 dalLoginRedirect(dalReturn),8 unauthorizedRedirectPath,9 ),10 );11 return res.data;12}这是你在 Server Component(页面文件)里最常用的函数。
dalReturn 检查登录 检查权限 检查其他错误 成功则返回 data。const data = dalVerifySuccess(await getMyData())。如果有任何问题,用户会自动被送走;如果有数据,你就直接拿到了 data,不需要再写 if 判断。这是一个链式组合。它依次检查:是否没登录?(是就跳登录页) -> 是否没权限?(是就跳首页或其它页) -> 是否有其他错误?(是就直接抛异常)。
dalFormatErrorMessage(error)no-user)翻译成人类能看懂的话(“您需要登录才能执行此操作”)。为了帮你理清思路,请看下面这个逻辑流向图:
完整的使用流程如下:
定义层 (Data Layer):
使用 dalRequireAuth 包装你的 SQL 查询,确保用户登录后才能操作SQL。使用dalDbOperation包裹数据库操作,使用try...catch捕获错误,将返回结果或者错误包装成对象返回。
处理层 (Action/Page):
dalVerifySuccess:遇到错误自动重定向,成功直接拿数据。result.success:如果失败,用 dalFormatErrorMessage 拿到文字发回给前端显示。这样写的最大好处: 你的业务逻辑(SQL)只需要写一次,但在“直接跳页面”和“只显示报错文字”两个场景下都能复用。
- 逻辑流示意图
为了让你理解这些函数是怎么串联的,看这个流程:
① UI 发起请求 调用一个 DAL 函数。
② DAL 函数内部 调用
dalRequireAuth。③
dalRequireAuth检查权限 失败则返回错误对象 成功则调用dalDbOperation。④
dalDbOperation执行 SQL 成功则返回{ success: true, data }。⑤ UI 拿到结果 调用
dalVerifySuccess。
- 如果是错误对象 自动触发
redirect。- 如果是成功对象 拿到
data开始渲染。
- 为什么这样写是“资深”?
这段代码解决了一个 Next.js 开发中的巨大难题:灵活性。
如果你在 Page 里用:你可以用
dalVerifySuccess,报错了直接跳页面。如果你在 Server Action (表单提交) 里用:你可能不想跳页面,你想给用户显示个红色的文字提醒。这时你就不用
dalVerifySuccess,而是手动解构结果:21const res = await getTodos()2if (!res.success) return { error: dalFormatErrorMessage(res.error) }解耦:这段代码让“获取数据”和“处理错误(跳转还是弹窗)”彻底分开了。
总结
dalRequireAuth和dalDbOperation是在定义数据函数时用的(保驾护航)。dalVerifySuccess等是在页面/组件里调用函数时用的(决定怎么对待结果)。
这是一个非常棒的问题,它触及了 dal-advanced 模式的核心价值:灵活性。
dal-advanced 并不强制你报错的方式,它只是给了你一份“标准化的诊断报告”,由你决定是在大屏幕上显示(Error 页面),还是弹一个小窗口(Toast)。
error.tsx (全屏报错)如果你希望 Next.js 捕捉到错误并跳转到错误页面,你会使用 dalThrowError 或 dalVerifySuccess。
只要一 throw,Next.js 就会自动激活最近的 error.tsx。
场景:加载一个至关重要的页面数据(如订单详情)。如果拿不到数据,页面没法渲染。
代码逻辑:
x
1// page.tsx (Server Component)2export default async function OrderPage() {3 const result = await getOrderDetails(id);45 // 如果 result.success 是 false,dalVerifySuccess 会抛出(throw)错误6 // 只要一 throw,Next.js 就会自动激活最近的 error.tsx7 const data = dalVerifySuccess(result); 89 return <OrderView data={data} />;10}Toast (轻量提醒)如果你在 Server Action(比如用户点击“收藏”按钮)中使用,你通常不希望整个页面刷新报错,而是想在角落弹出一个 Toast。
场景:用户没登录就点收藏,或者数据库写操作失败。
代码逻辑:
xxxxxxxxxx191// action.ts (Server Action)2export async function toggleFavorite(id: string) {3 const result = await toggleFavoriteInDb(id);45 if (!result.success) {6 // 我们不 throw,而是返回一个包含错误信息的对象7 return { 8 errorMessage: dalFormatErrorMessage(result.error) 9 };10 }1112 revalidatePath("/");13}1415// client-component.tsx16const res = await toggleFavorite(id);17if (res?.errorMessage) {18 toast.error(res.errorMessage); // 这里的 toast 就是页面上的小弹窗19}redirect (自动跳转)这是 dal-advanced 提供的第三种路径。
dalLoginRedirect。它既不会弹 Toast,也不会让页面崩溃进入 error.tsx,而是直接把用户“瞬移”到 /login 页面。我们可以通过下面这张图来看清错误流转的过程:
| 处理方式 | 使用的函数 | 最终 UI 表现 | 适用场景 |
|---|---|---|---|
| 硬拦截 | dalVerifySuccess | error.tsx 页面 | 必须有数据才能渲染的页面组件。 |
| 软处理 | dalFormatErrorMessage | Toast 或 表单文字 | 按钮点击、表单提交等异步交互。 |
| 重定向 | dalLoginRedirect | 浏览器 URL 变化,跳转页面 | 鉴权失败、权限不足。 |
因为在没有这个系统之前,如果你在数据库函数里写死了 throw new Error(),你就只能用 error.tsx 报错,没法轻松地改成用 Toast 提示。
有了 dal-advanced,数据库函数只管吐出结果,显示方式的选择权交给了调用它的那行代码。
比如说这段代码:
x
1export async function getAllTodos() {2 // cacheTag("todos")34 return dalRequireAuth(5 () =>6 dalDbOperation(() => {7 return db.query.TodoTable.findMany({8 with: {9 user: true,10 },11 })12 }),13 { allowedRoles: ["admin"] },14 )15}已经使用dalDbOperation和dalRequireAuth保护好了操作,如果有报错,那么就会返回error数据,为什么还需要使用dalVerifySuccess来检查一遍呢?我的感觉就是代码重复了。

简单回答:因为如果报错了,dalDbOperation和dalRequireAuth只能返回错误对象,没有办法对错误进行UI方面的处理。而dalVerifySuccess可以依据相应的错误类型,对错误进行UI上的处理,比如说返回login页面、或者给出toast提示并返回上一页等等。
以下是Gemini给出的回答:
这是一个非常敏锐的观察!初看之下,确实感觉“保镖”和“医生”做了两遍检查。
但实际上,这正是 dal-advanced 模式最精妙的地方:一个是逻辑拦截,一个是UI决策。 它们并不重复,而是互补的。
以下是深度拆解,告诉你为什么这两行代码缺一不可:
dalRequireAuth(在 getAllTodos 内部)角色:底线保镖
{ success: false, error: { type: "no-access" } }。if (res.error.type === "no-access")角色:定制化 UI 策略
if,这实际上是在拦截掉这种特定的错误,不让它传给后面的 dalVerifySuccess。dalVerifySuccess角色:兜底清理员
任务:处理那些“剩下的、我没想好怎么处理”的所有错误。
执行逻辑:
redirect 去登录页)。throw 报错)。todos 数据(它不仅做检查,还起到了“解构数据”的作用)。请看这个逻辑分流过程:
如果你删掉其中一个会发生什么?
删掉 dalRequireAuth (内部保镖):
数据泄漏。任何人调用这个查询都能拿到所有用户的 Todo。这是安全事故。
删掉组件里的 if 判断:
当没权限时,dalVerifySuccess 会直接抛出一个硬错误(导致用户看到 error.tsx 的全屏报错页)。你就失去了展示那个漂亮的 <h1>You do not have access...</h1> 提示的机会。
删掉 dalVerifySuccess:
你拿到的 res 依然是一个包含 success, data, error 的复杂对象。你没法直接拿 todos 去做 reduce 操作,TypeScript 会因为你不确定 success 是否为真而报错。
查询层(DAL):总是用 dalRequireAuth 封装,保证安全性。
页面层(Page):
if 拦截掉。dalVerifySuccess。它会帮我把“没登录”的用户送走,把“报错”的用户交给错误页,最后顺手把干净的数据解构出来给我用。作者在这里展示的是:如何利用 dal-advanced 模式,既保留了默认的重定向/报错功能,又能灵活地为特定错误提供自定义的 UI 渲染。
https://www.youtube.com/watch?v=ovnyeq-Xxrc
视频中 Kyle (Web Dev Simplified) 按顺序演示了 5 个递进的错误处理版本,核心场景是 Next.js 项目(server actions / API routes)中处理登录、权限等错误(如 Unauthenticated、Unauthorized、ValidationError、Unexpected 等)。他使用了自定义错误类、权限检查、redirect vs JSON 响应等典型示例。
直接在 server action 或 API route 里写业务逻辑 + try-catch,处理不同错误类型(redirect 或 return error message)。
典型代码(重建自演示风格):
xxxxxxxxxx281// app/login/action.ts (server action)2'use server'34export async function loginAction(formData: FormData) {5 try {6 const email = formData.get('email') as string;7 const password = formData.get('password') as string;89 // 业务逻辑 + 权限检查10 if (!email || !password) throw new ValidationError('Email and password required');11 const user = await db.login({ email, password });12 if (!user) throw new UnauthenticatedError();13 if (!user.hasPermission('admin')) throw new AuthorizationError();1415 redirect('/dashboard'); // success16 } catch (error) {17 if (error instanceof ValidationError) {18 return { error: error.message }; // for form19 }20 if (error instanceof UnauthenticatedError) {21 redirect('/login?error=unauthenticated');22 }23 if (error instanceof AuthorizationError) {24 redirect('/unauthorized');25 }26 return { error: 'Something went wrong' }; // unexpected27 }28}哪里好:
哪里不好:
把核心业务 + 权限检查抽到独立 service 文件,service 只负责返回数据或 throw custom errors,不处理 redirect/JSON。
典型代码:
xxxxxxxxxx271// services/authService.ts2export class ValidationError extends Error { }3export class UnauthenticatedError extends Error { }4export class AuthorizationError extends Error { }56export async function loginService(credentials: {email: string; password: string}) {7 if (!credentials.email || !credentials.password) {8 throw new ValidationError('Email and password required');9 }10 const user = await db.login(credentials);11 if (!user) throw new UnauthenticatedError();12 if (!user.hasPermission('admin')) throw new AuthorizationError();13 return user; // success14}1516// app/login/action.ts17export async function loginAction(formData: FormData) {18 try {19 const user = await loginService({ /*...*/ });20 redirect('/dashboard');21 } catch (error) {22 // 仍然需要相同的 if instanceof 链处理 redirect / return23 if (error instanceof ValidationError) return { error: error.message };24 if (error instanceof UnauthenticatedError) redirect('/login?error=unauthenticated');25 // ...26 }27}哪里好:
哪里不好:
Kyle 特别强调即使用了 service layer,问题依然存在:
好/不好:基本同上,只是更清晰地展示了为什么需要下一步。
不再 throw,而是返回 Result 类型(success 或 failure),用 discriminated union 让 TS 自动 narrowing。
典型代码(视频中使用 tuple 风格,简化版):
xxxxxxxxxx321// types/result.ts2type Result<S, E> = [S, null] | [null, E]; // tuple discriminated union34function ok<S, E = never>(data: S): Result<S, E> {5 return [data, null as never];6}78function error<S = never, E>(err: E): Result<S, E> {9 return [null as never, err];10}1112// services/authService.ts13export async function loginService() : Promise<Result<User, ValidationError | UnauthenticatedError | AuthorizationError>> {14 if (!credentials.email ) return error(new ValidationError());15 // ...16 if (!user) return error(new UnauthenticatedError());17 if (!hasPermission) return error(new AuthorizationError());18 return ok(user);19}2021// action22const result = await loginService();23if (result[1] !== null) { // error case24 const err = result[1];25 if (err instanceof ValidationError) return { error: err.message };26 if (err instanceof UnauthenticatedError) redirect('/login?error=unauthenticated');27 // ...28 // 用 satisfies never 强制 exhaustive:29 // const _exhaustive: never = err; // 如果漏掉分支,编译错误30}31const user = result[0]; // success32redirect('/dashboard');哪里好:
satisfies never 可在编译时强制处理所有错误分支(exhaustive checking)。哪里不好:
{ok: true, value} 或 {ok: false, error})。
引入 neverthrow(轻量,无依赖),提供 Ok/Err、match、andThen、map 等方法,支持 AsyncResult。
典型代码:
x
1import { ok, err, Result } from 'neverthrow';23async function loginService(): Promise<Result<User, CustomErrorUnion>> {4 if () return err(new ValidationError());5 // ...6 return ok(user);7}89const result = await loginService();10result.match({11 ok: (user) => redirect('/dashboard'),12 err: (error) => {13 if (error instanceof UnauthenticatedError) redirect('/login?...');14 // exhaustive with satisfies15 }16});1718// 链式示例19result20 .andThen((user) => someNextOperation(user)) // 只在 ok 时执行21 .match({ ok: , err: });还提到 ESLint 插件强制使用 Result(禁止直接 throw 或忽略错误)。
哪里好:
哪里不好:
总体:Kyle 的讲解非常清晰,从问题到方案递进强烈,推荐中高级 TS/Next.js 开发者观看。核心理念是“错误作为显式值 + 类型安全 + 架构分离”。没有专用 repo,代码是演示性质的。
在 Next.js 的 App Router(Server Components) 中,服务端发出的 fetch 请求不会出现在浏览器的 Network 面板里,这是目前(2025年底)框架的设计使然。但还是可以手动输出一些日志来调试。
1、在 next.config.js 里打开 fetch 日志(Next.js 内置,最方便)
xxxxxxxxxx111/** @type {import('next').NextConfig} */2const nextConfig = {3 logging: {4 fetches: {5 fullUrl: true, // 显示完整 url6 hmrRefreshes: true // 连热更新时缓存命中的请求也显示7 }8 }9}1011module.exports = nextConfig2、加上基本的错误捕获 + 丰富日志
xxxxxxxxxx341export default async function Home() {2 let data = null;3 let error = null;45 try {6 const start = Date.now();7 const res = await fetch("https://....", {8 cache: "no-store" // 方便调试9 });10 11 const duration = Date.now() - start;12 13 console.log("[FETCH]", {14 url: res.url,15 status: res.status,16 ok: res.ok,17 time: `${duration}ms`,18 cacheStatus: res.headers.get("x-nextjs-cache") || "unknown"19 });2021 if (!res.ok) throw new Error(`HTTP ${res.status}`);2223 data = await res.json();24 } catch (err) {25 error = err;26 console.error("[FETCH_ERROR]", {27 message: err.message,28 stack: err.stack?.split("\n").slice(0, 5),29 cause: err.cause30 });31 }3233 // ... 后面正常渲染34}你会看到类似这样清晰的输出:
xxxxxxxxxx121[FETCH] {2 url: 'https://random-word-api.herokuapp.com/word',3 status: 502,4 ok: false,5 time: '1243ms',6 cacheStatus: 'skip'7}8[FETCH_ERROR] {9 message: 'fetch failed',10 stack: [ ... ],11 cause: { code: 'ECONNRESET', ... }12}